using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;

namespace Universe.Threading
{
    public delegate void ForEachDelegate<T>(int index, T element);

    public delegate void Action();
    public delegate void Action<T>(T arg);
    public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
    public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);
    public delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

    public class Parallel
    {

        public static void Do(params Action[] actions)
        {
            ForEach(actions, delegate(int index, Action action) { action(); });
        }

        public static void Do<T>(T arg, params Action<T>[] actions)
        {
            ForEach(actions, delegate(int index, Action<T> action) { action(arg); });
        }

        public static void Do<T1, T2>(T1 arg1, T2 arg2, params Action<T1, T2>[] actions)
        {
            ForEach(actions, delegate(int index, Action<T1, T2> action) { action(arg1, arg2); });
        }

        public static void Do<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3, params Action<T1, T2, T3>[] actions)
        {
            ForEach(actions, delegate(int index, Action<T1, T2, T3> action) { action(arg1, arg2, arg3); });
        }

        public static void Do<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, params Action<T1, T2, T3, T4>[] actions)
        {
            ForEach(actions, delegate(int index, Action<T1, T2, T3, T4> action) { action(arg1, arg2, arg3, arg4); });
        }

        public static void ForEach<T>(IEnumerable<T> collection, ForEachDelegate<T> loopbody)
        {
            using (ManualResetEvent mre = new ManualResetEvent(false))
            {
                int index = 0;
                List<InvocationException<T>> exceptions = new List<InvocationException<T>>();
                int processed = 1;
                IEnumerator<T> en = collection.GetEnumerator();
                try
                {
                    while (en.MoveNext())
                    {
                        T element = en.Current;

                        Interlocked.Increment(ref processed);
                        int localIndex = index;
                        ThreadPool.QueueUserWorkItem(
                            delegate
                                {
                                    try
                                    {
                                        loopbody(localIndex, element);
                                    }
                                    catch (Exception ex)
                                    {
                                        lock (exceptions)
                                            exceptions.Add(
                                                new InvocationException<T>("Failed to invoke Method " + loopbody.Method + " on element #" + localIndex + " " + element, ex,
                                                                           localIndex, element));
                                    }

                                    if (Interlocked.Decrement(ref processed) == 0)
                                        mre.Set();
                                });

                        index++;
                        lock (exceptions)
                            if (exceptions.Count > 0)
                                throw exceptions[0];
                    }

                    if (Interlocked.Decrement(ref processed) == 0)
                        mre.Set();

                    mre.WaitOne();
                }
                finally
                {
                    en.Dispose();
                }

                if (exceptions.Count > 0)
                    throw exceptions[0];

            }
        }

        public static void ForEach<T>(IEnumerable<T> collection, int threadsLimit, ForEachDelegate<T> loopbody)
        {
            if (threadsLimit == 1)
            {
                ExecSync(collection, loopbody);
                return;
            }

            using (Semaphore sem = new Semaphore(threadsLimit, threadsLimit))
            using (ManualResetEvent mre = new ManualResetEvent(false))
            {
                bool aborted = false;
                int processed = 1;
                IEnumerator<T> enumerator = collection.GetEnumerator();
                int index = 0;
                List<InvocationException<T>> exceptions = new List<InvocationException<T>>();
                try
                {
                    while (enumerator.MoveNext())
                    {
                        T element = enumerator.Current;

                        sem.WaitOne();
                        if (!aborted)
                        {
                            Interlocked.Increment(ref processed);
                            int localIndex = index;
                            ThreadPool.QueueUserWorkItem(
                                delegate
                                    {
                                        if (!aborted)
                                        {
                                            try
                                            {
                                                loopbody(localIndex, element);
                                            }
                                            catch (Exception ex)
                                            {
                                                aborted = true;
                                                lock (exceptions)
                                                    exceptions.Add(
                                                        new InvocationException<T>(
                                                            "Failed to invoke Method " + loopbody.Method +
                                                            " on element #"
                                                            + localIndex + " " + element,
                                                            ex,
                                                            localIndex,
                                                            element));
                                            }
                                        }

                                        sem.Release();

                                        if (Interlocked.Decrement(ref processed) == 0)
                                            mre.Set();
                                    });
                        }

                        lock(exceptions)
                            if (exceptions.Count > 0)
                                break;

                        index++;
                    }

                    if (Interlocked.Decrement(ref processed) == 0)
                        mre.Set();

                    mre.WaitOne();
                }
                finally
                {
                    enumerator.Dispose();
                }

                if (exceptions.Count > 0)
                    throw exceptions[0];
            }
        }

        public static void ForEach<T>(IEnumerable<T> collection, int threadsLimit, int granularity, ForEachDelegate<T> loopbody)
        {
            if (threadsLimit == 1)
            {
                ExecSync(collection, loopbody);
                return;
            }

            using (Semaphore sem = new Semaphore(threadsLimit, threadsLimit))
            using (ManualResetEvent mre = new ManualResetEvent(false))
            {
                bool aborted = false;
                int index = 0;
                List<InvocationException<T>> exceptions = new List<InvocationException<T>>();
                Args<T> args = new Args<T>(threadsLimit, granularity);
                TArray<T> array = args.Take();
                int pos = 0;
                int processed = 1;
                IEnumerator<T> enumerator = collection.GetEnumerator();
                try
                {
                    while (enumerator.MoveNext())
                    {
                        T element = enumerator.Current;
                        array.Content[pos++] = element;
                        if (pos == granularity)
                        {
                            Interlocked.Increment(ref processed);
                            sem.WaitOne();
                            int length = pos;
                            TArray<T> workarea = array;
                            pos = 0;
                            array = args.Take();
                            int localIndex = index;
                            ThreadPool.QueueUserWorkItem(
                                delegate
                                    {
                                        if (!aborted)
                                        {
                                            for (int k = 0; k < length; k++)
                                            {
                                                try
                                                {
                                                    loopbody(localIndex + k, workarea.Content[k]);
                                                }
                                                catch (Exception ex)
                                                {
                                                    aborted = true;
                                                    lock (exceptions)
                                                        exceptions.Add(
                                                            new InvocationException<T>(
                                                                "Failed to invoke Method " + loopbody.Method
                                                                + " on element #" + (localIndex + k) + " "
                                                                + workarea.Content[k],
                                                                ex,
                                                                localIndex + k,
                                                                workarea.Content[k]));
                                                }
                                            }
                                        }

                                        args.Back(workarea);
                                        sem.Release();

                                        if (Interlocked.Decrement(ref processed) == 0)
                                            mre.Set();
                                    });

                            index += granularity;
                            lock (exceptions)
                                if (exceptions.Count > 0)
                                    break;
                        }
                    }

                    if (pos > 0 && !aborted)
                    {
                        Interlocked.Increment(ref processed);
                        sem.WaitOne();
                        int length = pos;
                        TArray<T> workarea = array;
                        int localIndex = index;
                        ThreadPool.QueueUserWorkItem(
                            delegate
                                {
                                    if (!aborted)
                                    {
                                        for (int k = 0; k < length; k++)
                                        {
                                            try
                                            {
                                                loopbody(localIndex + k, workarea.Content[k]);
                                            }
                                            catch (Exception ex)
                                            {
                                                aborted = true;
                                                lock (exceptions)
                                                    exceptions.Add(
                                                        new InvocationException<T>(
                                                            "Failed to invoke Method " + loopbody.Method +
                                                            " on element #" + (localIndex + k) + " " +
                                                            workarea.Content[k], ex,
                                                            localIndex + k,
                                                            workarea.Content[k]));
                                            }
                                        }
                                    }

                                    args.Back(workarea);
                                    sem.Release();

                                    if (Interlocked.Decrement(ref processed) == 0)
                                        mre.Set();
                                });
                    }

                    if (Interlocked.Decrement(ref processed) == 0)
                        mre.Set();

                    mre.WaitOne();
                }
                finally
                {
                    enumerator.Dispose();
                }

                if (exceptions.Count > 0)
                    throw exceptions[0];
            }
        }

        
        private static void ExecSync<T>(IEnumerable<T> collection, ForEachDelegate<T> loopbody)
        {
            int index = 0;
            foreach (T element in collection)
            {
                try
                {
                    loopbody(index, element);
                }
                catch(Exception ex)
                {
                    throw new InvocationException<T>(
                        "Failed to invoke Method " 
                        + loopbody.Method 
                        + " on element #"
                        + (index) + " " + element,
                        ex, 
                        index,
                        element);
                }

                index++;
            }
        }
        
        // one array per thread using Args' collection
        class Args<T>
        {
            object _sync;
            Dictionary<int, TArray<T>> _bufferBusy;
            private List<TArray<T>> _bufferFree;
            private int _threads;
            private int _granularity;
            private int _counter;

            Dictionary<int, object> _dump;

            public Args(int threads, int granularity)
            {
                _threads = threads;
                _granularity = granularity;

                _sync = new object();
                _bufferBusy = new Dictionary<int, TArray<T>>();
                _bufferFree = new List<TArray<T>>();
                _dump = new Dictionary<int, object>();
                _counter = 0;
            }

            // always called syncronously
            public TArray<T> Take()
            {
                lock(_sync)
                {
                    if (_bufferFree.Count > 0)
                    {
                        TArray<T> ret = _bufferFree[_bufferFree.Count - 1];
                        _bufferFree.RemoveAt(_bufferFree.Count - 1);
                        _bufferBusy[ret.Id] = ret;
                        // Debug007.WriteLine("Take reused buffer #" + ret.Id);
                        return ret;
                    }

                    TArray<T> item = new TArray<T>(_counter++, _granularity);
                    _bufferBusy[item.Id] = item;
                    _dump[item.Id] = null;
                    return item;
                }

            }

            public void Back(TArray<T> content)
            {
                int failed = -1;
                lock (_sync)
                {
                    _bufferFree.Add(content);
                    if (!_bufferBusy.Remove(content.Id))
                    {
                        failed = content.Id;
                    }
                }
            }
        }

        class TArray<T>
        {
            public readonly int Id;
            public readonly T[] Content;

            public TArray(int id, int granularity)
            {
                Id = id;
                Content = new T[granularity];
            }
        }

    }
}
